<?php


/**
 * 邮件发送类.
 *
 * @author Qian Su <aoxue.1988.su.qian@163.com>
 * @copyright ©2003-2103 phpwind.com
 * @license http://www.windframework.com
 *
 * @version $Id: WindMail.php 3904 2013-01-08 07:01:26Z yishuo $
 */
class WindMail
{
    /**
     * @var array 邮件头
     */
    private $mailHeader = array();
    /**
     * @var array 邮件附件
     */
    private $attachment = array();
    /**
     * @var string 邮件字符集
     */
    private $charset = 'utf-8';
    /**
     * @var string 是否是内嵌资源
     */
    private $embed = false;
    /**
     * @var array 邮件收件人
     */
    private $recipients = null;
    /**
     * @var string 邮件发件人
     */
    private $from = '';
    /**
     * @var string 邮件消息体html展现方式
     */
    private $bodyHtml = '';
    /**
     * @var string 邮件消息体文本展现方式
     */
    private $bodyText = '';
    /**
     * @var array 邮件边界线
     */
    private $boundary;
    /**
     * @var string 邮件编码方式
     */
    private $encode = self::ENCODE_BASE64;
    /**
     * @var 内容类型
     */
    private $contentType;

    // 常用邮件MIME
    const CRLF = "\n";
    const TO = 'To';
    const CC = 'Cc';
    const BCC = 'Bcc';
    const FROM = 'From';
    const SUBJECT = 'Subject';
    const MESSAGEID = 'Message-Id';
    const CONTENTTYPE = 'Content-Type';
    const CONTENTENCODE = 'Content-Transfer-Encoding';
    const CONTENTID = 'Content-ID';
    const CONTENTPOSITION = 'Content-Disposition';
    const CONTENTDESCRIPT = 'Content-Description';
    const CONTENTLOCATION = 'Content-Location';
    const CONTENTLANGUAGE = 'Content-Language';
    const DATE = 'Date';

    // 邮件MIME类型
    const MIME_OCTETSTREAM = 'application/octet-stream';
    const MIME_TEXT = 'text/plain';
    const MIME_HTML = 'text/html';
    const MIME_ALTERNATIVE = 'multipart/alternative';
    const MIME_MIXED = 'multipart/mixed';
    const MIME_RELATED = 'multipart/related';

    // 邮件编码
    const ENCODE_7BIT = '7bit';
    const ENCODE_8BIT = '8bit';
    const ENCODE_QP = 'quoted-printable';
    const ENCODE_BASE64 = 'base64';
    const ENCODE_BINARY = 'binary';

    // 邮件编码内容
    const DIS_ATTACHMENT = 'attachment';
    const DIS_INLINE = 'inline';
    const LINELENGTH = 72;

    // 邮件发送方式
    const SEND_SMTP = 'smtp';
    const SEND_PHP = 'php';
    const SEND_SEND = 'send';

    /**
     * 发送邮件.
     *
     * @param string $type   发送类型
     * @param array  $config 邮件发送器需要的配置数据
     *
     * @throws Exception
     *
     * @return bool
     */
    public function send($type = self::SEND_SMTP, $config = array())
    {
        $class = 'Wind'.ucfirst($type).'Mail';
        /* @var $sender IWindSendMail */
        $sender = WindFactory::createInstance($class);

        return $sender->send($this, $config);
    }

    /**
     * 创建邮件头信息.
     *
     * @return string
     */
    public function createHeader()
    {
        if (!isset($this->mailHeader[self::CONTENTTYPE])) {
            $type = self::MIME_TEXT;
            if ($this->attachment) {
                $type = $this->embed ? self::MIME_RELATED : self::MIME_MIXED;
            } elseif ($this->bodyHtml) {
                $type = $this->bodyText ? self::MIME_ALTERNATIVE : self::MIME_HTML;
            }
            $this->setContentType($type);
        }
        if (!isset($this->mailHeader[self::CONTENTENCODE])) {
            $this->setContentEncode();
        }
        $header = '';
        foreach ($this->mailHeader as $key => $value) {
            if (!$value) {
                continue;
            }
            $header .= $key.': ';
            if (is_array($value)) {
                foreach ($value as $_key => $_value) {
                    $header .= (is_string($_key) ? $_key.' '.$_value : $_value).',';
                }
                $header = trim($header, ',');
            } else {
                $header .= $value;
            }
            $header .= self::CRLF;
        }

        return $header.self::CRLF;
    }

    /**
     * 创建邮件消息体.
     *
     * @return string
     */
    public function createBody()
    {
        $body = '';
        switch ($this->contentType) {
            case self::MIME_TEXT:
                $body = $this->_encode($this->bodyText).self::CRLF;
                break;
            case self::MIME_HTML:
                $body = $this->_encode($this->bodyHtml).self::CRLF;
                break;
            case self::MIME_ALTERNATIVE:
                $body = $this->_createBoundary($this->_boundary(), 'text/plain');
                $body .= $this->_encode($this->bodyText).self::CRLF;
                $body .= $this->_createBoundary($this->_boundary(), 'text/html');
                $body .= $this->_encode($this->bodyHtml).self::CRLF;
                $body .= $this->_boundaryEnd($this->_boundary());
                break;
            default:
                $body .= $this->_boundaryStart($this->_boundary());
                $body .= sprintf('Content-Type: %s;%s'."\tboundary=\"%s\"%s",
                    'multipart/alternative', self::CRLF, $this->_boundary(1),
                    self::CRLF.self::CRLF);
                $body .= $this->_createBoundary($this->_boundary(1), 'text/plain').self::CRLF;
                $body .= $this->_encode($this->bodyText).self::CRLF.self::CRLF;
                $body .= $this->_createBoundary($this->_boundary(1), 'text/html').self::CRLF;
                $body .= $this->_encode($this->bodyHtml).self::CRLF.self::CRLF;
                $body .= $this->_boundaryEnd($this->_boundary(1));
                $body .= $this->_attach();
                break;
        }

        return $body;
    }

    /**
     * 设置发件人.
     *
     * @param string $email 发件人邮箱
     * @param string $name  发件人姓名
     */
    public function setFrom($email, $name = null)
    {
        if (!$email || !is_string($email)) {
            return;
        }
        $this->from = $email;
        $name && $email = $this->_encodeHeader($name).' <'.$email.'>';
        $this->setMailHeader(self::FROM, $email, false);
    }

    /**
     * 取得发件人.
     *
     * @return string
     */
    public function getFrom()
    {
        return $this->from;
    }

    /**
     * 设置收件人.
     *
     * @param string|array $email 收件人邮箱
     * @param string       $name  收件人姓名
     */
    public function setTo($email, $name = null)
    {
        if (!$email) {
            return;
        }
        $email = $this->_setRecipientMail($email, $name);
        $this->setMailHeader(self::TO, $email);
    }

    /**
     * 取得收件人.
     *
     * @return array
     */
    public function getTo()
    {
        return $this->getMailHeader(self::TO);
    }

    /**
     * 设置抄送人.
     *
     * @param string $email 抄送人邮箱
     * @param string $name  抄送人姓名
     */
    public function setCc($email, $name = null)
    {
        if (!$email) {
            return;
        }
        $email = $this->_setRecipientMail($email, $name);
        $this->setMailHeader(self::CC, $email);
    }

    /**
     * 取得抄送的对象
     *
     * @return array
     */
    public function getCc()
    {
        return $this->getMailHeader(self::CC);
    }

    /**
     * 设置暗送人.
     *
     * @param string $email 暗送人邮箱
     * @param string $name  暗送人姓名
     */
    public function setBcc($email, $name = null)
    {
        if (!$email) {
            return;
        }
        $email = $this->_setRecipientMail($email, $name);
        $this->setMailHeader(self::BCC, $email);
    }

    /**
     * 取得暗送对象
     *
     * @return array
     */
    public function getBcc()
    {
        return $this->getMailHeader(self::BCC);
    }

    /**
     * 设置邮件主题.
     *
     * @param string $subject 主题
     */
    public function setSubject($subject)
    {
        $this->setMailHeader(self::SUBJECT, $this->_encodeHeader($subject), false);
    }

    /**
     * 取得邮件主题.
     *
     * @return string
     */
    public function getSubject()
    {
        $subject = $this->getMailHeader(self::SUBJECT);
        is_array($subject) && $subject = $subject[0];

        return str_replace(array("\r", "\n"), array('', ' '), $subject);
    }

    /**
     * 设置邮件日期
     *
     * @param string $data
     */
    public function setDate($date)
    {
        $this->setMailHeader(self::DATE, $date);
    }

    /**
     * 设置邮件头.
     *
     * @param string $name   邮件头名称
     * @param string $value  邮件头对应的值
     * @param bool   $append 是否是追加
     */
    public function setMailHeader($name, $value, $append = true)
    {
        is_array($value) || $value = array($value);
        if (false === $append || !isset($this->mailHeader[$name])) {
            $this->mailHeader[$name] = $value;
        } else {
            foreach ($value as $key => $_value) {
                if (is_string($key)) {
                    $this->mailHeader[$name][$key] = $_value;
                } else {
                    $this->mailHeader[$name][] = $_value;
                }
            }
        }
    }

    /**
     * 返回邮件头信息值
     *
     * @param string $name
     */
    public function getMailHeader($name)
    {
        if (!$name) {
            return $this->mailHeader;
        }

        return isset($this->mailHeader[$name]) ? $this->mailHeader[$name] : array();
    }

    /**
     * 设置邮件消息ID.
     */
    public function setMessageId()
    {
        $user = array_pop($this->getFrom());
        $user || $user = getmypid();
        if ($recipient = $this->getRecipients()) {
            $recipient = array_rand($recipient);
        } else {
            $recipient = 'No recipient';
        }
        $host = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : php_uname('n');
        $message = sha1(time().$user.mt_rand().$recipient).'@'.$host;
        $this->setMailHeader(self::MESSAGEID, '<'.$message.'>');
    }

    /**
     * 设置邮件编码
     *
     * @param string $encode
     */
    public function setContentEncode($encode = self::ENCODE_BASE64)
    {
        $this->encode = $encode;
        $this->setMailHeader(self::CONTENTENCODE, $encode);
    }

    /**
     * 设置邮件类型.
     *
     * @param string $type
     */
    public function setContentType($type = self::MIME_TEXT)
    {
        if (self::MIME_TEXT == $type || self::MIME_HTML == $type) {
            $contentType = sprintf('%s; charset="%s"', $type, $this->charset);
        } elseif (self::MIME_RELATED == $type) {
            $contentType = sprintf('%s;%s type="text/html";%s boundary="%s"',
                self::MIME_RELATED, self::CRLF, self::CRLF, $this->_boundary());
        } else {
            $contentType = sprintf('%s;%s boundary="%s"', $type, self::CRLF, $this->_boundary());
        }
        $this->contentType = $type;
        $this->setMailHeader(self::CONTENTTYPE, $contentType, false);
    }

    /**
     * 上传附件.
     *
     * @return string
     */
    private function _attach()
    {
        $attach = '';
        foreach ($this->attachment as $key => $value) {
            list($stream, $mime, $disposition, $encode, $filename, $cid) = $value;
            $filename || $filename = 'attachment_'.$key;
            $attach .= $this->_boundaryStart($this->_boundary());
            $attach .= sprintf(self::CONTENTTYPE.': %s; name="%s"%s', $mime, $filename,
                self::CRLF);
            $attach .= sprintf(self::CONTENTENCODE.': %s%s', $encode, self::CRLF);
            if ($disposition == 'inline') {
                $attach .= sprintf(self::CONTENTID.': <%s>%s', $cid, self::CRLF);
            }
            $attach .= sprintf(self::CONTENTPOSITION.': %s; filename="%s"%s%s', $disposition,
                $filename, self::CRLF, self::CRLF);
            $attach .= $this->_encode($stream, $encode).self::CRLF;
        }
        $attach .= $this->_boundaryEnd($this->_boundary());

        return $attach;
    }

    /**
     * 取得下一个quoted-printable.
     *
     * @param string $string
     *
     * @return string
     */
    private static function getNextQpToken($string)
    {
        return '=' == substr($string, 0, 1) ? substr($string, 0, 3) : substr($string, 0, 1);
    }

    /**
     * 获取边界线
     *
     * @return string
     */
    private function _createBoundary($boundary, $contentType, $charset = '', $encode = '')
    {
        $result = '';
        $charset || $charset = $this->charset;
        $encode || $encode = $this->encode;
        $result .= $this->_boundaryStart($boundary);
        $result .= sprintf(self::CONTENTTYPE.': %s; charset="%s"', $contentType, $charset);
        $result .= self::CRLF;
        $result .= sprintf(self::CONTENTENCODE.': %s%s', $encode, self::CRLF);
        $result .= self::CRLF;

        return $result;
    }

    /**
     * @param boundary
     *
     * @return string
     */
    private function _boundaryStart($boundary)
    {
        return '--'.$boundary.self::CRLF;
    }

    /**
     * 获取结束边界线
     *
     * @return string
     */
    private function _boundaryEnd($boundary)
    {
        return self::CRLF.'--'.$boundary.'--'.self::CRLF;
    }

    /**
     * 设置并返回边界线
     *
     * @param int $i 默认值为0
     *
     * @return string
     */
    private function _boundary($i = 0)
    {
        if (!$this->boundary) {
            $uniq_id = md5(uniqid(time()));
            $this->boundary[0] = 'b1_'.$uniq_id;
            $this->boundary[1] = 'b2_'.$uniq_id;
        }

        return $i == 1 ? $this->boundary[1] : $this->boundary[0];
    }

    /**
     * 编码邮件内容.
     *
     * @param string $message
     * @param string $encode
     *
     * @return string
     */
    private function _encode($message, $encode = '')
    {
        return $this->_getEncoder($encode)->encode(trim($message), self::LINELENGTH, self::CRLF);
    }

    /**
     * 编码邮件头部.
     *
     * @param string $message
     * @param string $encode
     *
     * @return string
     */
    private function _encodeHeader($message, $encode = '')
    {
        $message = strtr(trim($message), array("\r" => '', "\n" => '', "\r\n" => ''));

        return $this->_getEncoder($encode)->encodeHeader($message, $this->charset, self::LINELENGTH,
            self::CRLF);
    }

    /**
     * 根据当前编码获取邮件编码器，并返回邮件编码器对象
     *
     * @param encode
     *
     * @return IWindMailEncoder
     */
    private function _getEncoder($encode)
    {
        $encode || $encode = $this->encode;
        switch ($encode) {
            case self::ENCODE_QP:
                $mailEncoder = 'WindMailQp';
                break;
            case self::ENCODE_BASE64:
                $mailEncoder = 'WindMailBase64';
                break;
            case self::ENCODE_7BIT:
            case self::ENCODE_8BIT:
            default:
                $mailEncoder = 'WindMailBinary';
                break;
        }
        if (!class_exists($mailEncoder)) {
            throw new WindMailException(
            '[mail.WindMail._encode] encod class for '.$encode.' is not exist.');
        }

        return new $mailEncoder();
    }

    /**
     * @param string $email
     * @param string $name
     */
    private function _setRecipientMail($email, $name)
    {
        $_email = '';
        if (is_array($email)) {
            foreach ($email as $_e => $_n) {
                $_email .= $_n ? $this->_encodeHeader($_n).' <'.$_e.'>' : $_e;
                $this->recipients[] = $_e;
            }
        } else {
            $_email = $name ? $this->_encodeHeader($name).' <'.$email.'>' : $email;
            $this->recipients[] = $email;
        }

        return $_email;
    }

    /**
     * 取得真实的收件人.
     *
     * @return array
     */
    public function getRecipients()
    {
        return $this->recipients;
    }

    /**
     * 设置附件.
     *
     * @param string $stream      附件名或者附件内容
     * @param string $mime        附件类型
     * @param string $disposition 附件展现方式
     * @param string $encode      附件编码
     * @param string $filename    文件名
     * @param string $cid         内容ID
     */
    public function setAttachment($stream, $mime = self::MIME_OCTETSTREAM, $disposition = self::DIS_ATTACHMENT, $encode = self::ENCODE_BASE64, $filename = null, $cid = 0)
    {
        $this->attachment[] = array($stream, $mime, $disposition, $encode, $filename, $cid);
    }

    /**
     * 设置邮件展示内容.
     *
     * @param string $body
     */
    public function setBody($body)
    {
        $this->bodyHtml = $body;
    }

    /**
     * 设置邮件文本展示内容.
     *
     * @param string $bodyText
     */
    public function setBodyText($bodyText)
    {
        $this->bodyText = $bodyText;
    }

    /**
     * 设置邮件字符.
     *
     * @param string $charset
     */
    public function setCharset($charset)
    {
        $this->charset = $charset;
    }

    /**
     * 设置是否是内嵌资源.
     *
     * @param bool $embed
     */
    public function setEmbed($embed = false)
    {
        $this->embed = $embed;
    }
}
